Skip to main content

8_1. References

A reference lets you access a value without owning it.

  • Written as &T or &mut T
  • Does not take ownership
  • Does not free memory
  • Must always be valid

References are Rust’s safe alternative to raw pointers.

Why References Exist

Without references:

fn print(s: String) {
println!("{}", s);
}

let s = String::from("hello");
print(s);
// s is now gone

That’s annoying and inefficient.

With references:

fn print(s: &String) {
println!("{}", s);
}

let s = String::from("hello");
print(&s);
// s still exists

References enable safe sharing.

References and Ownership

Ownership Rule

References never own data.

let s = String::from("hello");
let r = &s;
  • s owns the heap allocation
  • r just points to it
  • When s is dropped, r becomes invalid (and Rust prevents this)

Immutable References (&T)

What they allow

  • Read access
  • Multiple references at the same time
let s = String::from("hello");

let r1 = &s;
let r2 = &s;

println!("{} {}", r1, r2);

Why this is safe

  • No one can modify s
  • No data races
  • No invalid memory access

Mutable References (&mut T)

What they allow

  • Read + write access
  • Exactly one mutable reference at a time
let mut s = String::from("hello");

let r = &mut s;
r.push_str(" world");

println!("{}", r);

Why this is restricted

  • Two writers = race conditions
  • Writer + reader = inconsistent reads

Rust blocks this at compile time.

Immutable vs Mutable Rule (Critical)

At any moment, either:

  • Many immutable references
  • One mutable reference

Both at the same time

let r1 = &s;
let r2 = &mut s; // ❌ compile error

This rule is the foundation of Rust’s thread safety.

Reference Scope (Non-Lexical Lifetimes)

Rust tracks actual usage, not just braces.

let mut s = String::from("hello");

let r1 = &s;
println!("{}", r1); // last use

let r2 = &mut s; // ✅ allowed
r2.push_str("!");

Even though r1 is still in scope textually, Rust knows it’s no longer used.

References in Function Parameters

Immutable reference

fn length(s: &String) -> usize {
s.len()
}

Mutable reference

fn append(s: &mut String) {
s.push('!');
}

Usage

let mut s = String::from("hello");

let len = length(&s);
append(&mut s);

println!("{} ({})", s, len);

References vs Copy

References themselves are Copy:

let s = String::from("hello");

let r1 = &s;
let r2 = r1; // copy of reference
  • Both point to the same data
  • Borrowing rules still apply
  • Ownership does not change

Dangling References (Prevented)

Rust forbids references that outlive data.

let r: &String;

{
let s = String::from("hello");
r = &s; // ❌ error
}

Why?

  • s is dropped
  • Heap memory freed
  • r would dangle

Rust stops this at compile time.

References and the Heap

let s = String::from("hello");
let r = &s;

Memory layout:

Stack:
r ──→ s ──→ Heap("hello")
  • Reference points to stack value
  • Stack value points to heap
  • Lifetimes guarantee everything stays valid

References and Structs

Struct Holding References

struct Excerpt<'a> {
text: &'a str,
}

Excerpt cannot outlive the data it references

Usage:

let s = String::from("hello world");
let e = Excerpt { text: &s[0..5] };

println!("{}", e.text);

References and Pattern Matching

Moving (bad)

let opt = Some(String::from("hello"));

match opt {
Some(s) => println!("{}", s), // move
None => {}
}

Borrowing (good)

match &opt {
Some(s) => println!("{}", s),
None => {}
}

Slices Are References

let s = String::from("hello world");
let slice = &s[0..5];
  • &str is a reference
  • No allocation
  • No ownership transfer

Same for arrays:

let arr = [1, 2, 3];
let slice = &arr[..];

References vs Raw Pointers

FeatureReferenceRaw Pointer
Always valid
Null allowed
Borrow checked
Safe by default

Raw pointers exist (*const T, *mut T) but require unsafe.

Mental Model That Works

  • Ownership → who frees memory
  • References → who can access memory
  • Borrow checker → traffic cop
  • Lifetimes → how long access lasts

If the compiler allows it, the reference is guaranteed safe.